package app

import (
	"encoding/json"
	"github.com/containernetworking/cni/libcni"
	"github.com/containernetworking/cni/pkg/types"
	"io"
	"os"
	"sort"
	"strings"
)

const (
	defaultCniPath  = "./cni"
	defaultConfPath = "/etc/cni/net.d"
	defaultBinPath  = "/opt/cni/bin"
	binName         = "rscni"
)

type CniUtil struct {
	path   string
	parser *CniParser
}

func (_ *CniUtil) defaultRsCni() libcni.NetworkConfig {
	network := &types.NetConf{
		Type: "rscni",
	}
	bytes, err := json.Marshal(network)
	if err != nil {
		return libcni.NetworkConfig{}
	}

	result := libcni.NetworkConfig{
		Network: network,
		Bytes:   bytes,
	}

	return result
}

func (cu *CniUtil) Add() error {
	conf := cu.defaultRsCni()
	newConf := cu.parser.deepCopyConfList()
	if cu.parser.ContainPlugin(newConf, conf.Network.Type) {
		return nil
	}

	newConf.Plugins = append(newConf.Plugins, &conf)
	err := cu.parser.SaveToFile(newConf)
	if err != nil {
		return err
	}

	return nil
}

func (cu *CniUtil) CopyBin() error {
	reader, err := os.Open(defaultCniPath)
	if err != nil {
		return err
	}
	writer, err := os.OpenFile(cu.parser.binPath, os.O_CREATE|os.O_RDWR, 0777)
	if err != nil {
		return err
	}
	defer func() {
		writer.Close()
		reader.Close()
	}()

	_, err = io.Copy(writer, reader)
	if err != nil {
		return err
	}

	return nil
}

func (cu *CniUtil) Recover() error {
	newConf := cu.parser.deepCopyConfList()
	err := cu.parser.SaveToFile(newConf)
	if err != nil {
		return err
	}

	return nil
}

func NewCniUtil() *CniUtil {
	result := &CniUtil{}
	parser := NewParser()
	result.parser = parser

	return result
}

func NewCniUtilWithPath(conf, bin string) *CniUtil {
	result := &CniUtil{}
	parser := NewParseWithPath(conf, bin)
	result.parser = parser

	return result
}

type CniParser struct {
	confPath     string
	binPath      string
	usedConfName string
	usedConfList *libcni.NetworkConfigList
}

func (cp *CniParser) SaveToFile(data *libcni.NetworkConfigList) error {
	m := make(map[string]interface{})
	m["name"] = data.Name
	m["cniVersion"] = data.CNIVersion

	plugins := make([]map[string]interface{}, len(data.Plugins))
	for index, i := range data.Plugins {
		b, err := json.Marshal(i.Network)
		if err != nil {
			continue
		}

		mp := make(map[string]interface{})
		err = json.Unmarshal(b, &mp)
		if err != nil {
			continue
		}
		if mp["dns"] != nil && len(mp["dns"].(map[string]interface{})) == 0 {
			delete(mp, "dns")
		}
		if mp["ipam"] != nil && len(mp["ipam"].(map[string]interface{})) == 0 {
			delete(mp, "ipam")
		}

		plugins[index] = mp
	}
	m["plugins"] = plugins

	file, err := os.OpenFile(cp.usedConfName, os.O_RDWR|os.O_CREATE, 0666)
	if err != nil {
		return err
	}

	defer file.Close()

	err = file.Truncate(0)
	if err != nil {
		return err
	}

	r, err := json.MarshalIndent(m, "", "\t")
	if err != nil {
		return err
	}

	_, err = file.Write(r)
	if err != nil {
		return err
	}

	return nil
}

func (cp *CniParser) ContainPlugin(conf *libcni.NetworkConfigList, typ string) bool {
	for _, i := range conf.Plugins {
		if typ == i.Network.Type {
			return true
		}
	}

	return false
}

func (cp *CniParser) deepCopyConfList() *libcni.NetworkConfigList {
	result := &libcni.NetworkConfigList{}
	result.Name = cp.usedConfList.Name
	result.CNIVersion = cp.usedConfList.CNIVersion
	result.DisableCheck = cp.usedConfList.DisableCheck

	plugins := make([]*libcni.NetworkConfig, len(cp.usedConfList.Plugins))
	for index, i := range cp.usedConfList.Plugins {
		bt := make([]byte, len(i.Bytes))
		copy(bt, i.Bytes)
		plugin := &libcni.NetworkConfig{
			Bytes: bt,
			Network: &types.NetConf{
				CNIVersion:    i.Network.CNIVersion,
				Name:          i.Network.CNIVersion,
				Type:          i.Network.Type,
				Capabilities:  i.Network.Capabilities,
				IPAM:          i.Network.IPAM,
				DNS:           i.Network.DNS,
				RawPrevResult: i.Network.RawPrevResult,
				PrevResult:    i.Network.PrevResult,
			},
		}
		plugins[index] = plugin
	}
	result.Plugins = plugins

	bt := make([]byte, len(cp.usedConfList.Bytes))
	copy(bt, cp.usedConfList.Bytes)
	result.Bytes = bt

	return result
}

func (cp *CniParser) setUsedConfigList() error {
	files, err := libcni.ConfFiles(cp.confPath, []string{".conflist", ".conf", ".json"})
	if err != nil {
		return err
	}
	sort.Strings(files)

	switch {
	case strings.Contains(files[0], ".conflist"):
		list, err := libcni.ConfListFromFile(files[0])
		if err != nil {
			return err
		}
		cp.usedConfList = list
	default:
		file, err := libcni.ConfFromFile(files[0])
		if err != nil {
			return err
		}
		conf, err := libcni.ConfListFromConf(file)
		if err != nil {
			return err
		}
		cp.usedConfList = conf
	}

	cp.usedConfName = files[0]

	return nil
}

func NewParseWithPath(conf, bin string) *CniParser {
	cp := &CniParser{
		confPath: conf,
		binPath:  bin,
	}

	err := cp.setUsedConfigList()
	if err != nil {
		return nil
	}

	return cp
}

func NewParser() *CniParser {
	cp := &CniParser{
		confPath: defaultConfPath,
		binPath:  defaultBinPath + "/" + binName,
	}

	err := cp.setUsedConfigList()
	if err != nil {
		return nil
	}

	return cp
}
